﻿#region License

// PentaBiz - Sustainable Software Development Framework Copyright (C) 2013 Zoltán Csizmazia
// 
// This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
// 
// This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

namespace PentaBiz.Cloud.Services.DynamicService.CodeGeneration.Templating
{
    /// <summary>
    ///     Defines a code template
    /// </summary>
    [DebuggerDisplay("[{TemplatedLine} - Templates:{Templates.Count}")]
    public class TemplateEngine
    {
        private const string PatternRegionStart = "#region (@[a-z|A-Z|0-9].*)";
        private const string PatternRegionEnd = "#endregion";

        private const string PatternEnginePropertyTemplate =
            "@(ᐒ?[a-z|A-Z|0-9]*ᐒ)?(ßEngineß[_a-z|A-Z][_|a-z|A-Z|0-9]*)(ᐘ[a-z|A-Z|0-9]*ᐘ?)?";

        private const string PatternPropertyTemplate =
            "@(ᐒ?[a-z|A-Z|0-9]*ᐒ)?([_a-z|A-Z][_|a-z|A-Z|0-9]*)(ᐘ[a-z|A-Z|0-9]*ᐘ?)?";

        /// <summary>
        ///     The template prefix character (ᐒ)
        /// </summary>
        public const string TemplatePrefixCharacter = "ᐒ";

        /// <summary>
        ///     The template postfix character (ᐘ)
        /// </summary>
        public const string TemplatePostfixCharacter = "ᐘ";


        private static readonly Regex RegexRegionStart = new Regex(PatternRegionStart, RegexOptions.Compiled);
        private static readonly Regex RegexRegionEnd = new Regex(PatternRegionEnd, RegexOptions.Compiled);
        private static readonly Regex RegexProperty = new Regex(PatternPropertyTemplate, RegexOptions.Compiled);

        private static readonly Regex RegexEngineProperty = new Regex(PatternEnginePropertyTemplate,
                                                                      RegexOptions.Compiled);

        private readonly string _templatedLine;
        private readonly List<TemplateEngine> _templates = new List<TemplateEngine>();

        private bool _isMuted;

        /// <summary>
        ///     Initializes a new instance of the <see cref="TemplateEngine" /> class.
        /// </summary>
        /// <param name="templateLines">The lines.</param>
        /// <param name="index">The index.</param>
        public TemplateEngine(string[] templateLines, ref int index)
        {
            while (index < templateLines.Length)
            {
                //check for #region start
                if (RegexRegionStart.IsMatch(templateLines[index]))
                {
                    IsRegionStart = true;
                    string regionName = RegexRegionStart.Match(templateLines[index]).Groups[1].Value;
                    index++;
                    Templates.Add(new TemplateEngine(templateLines, ref index)
                        {
                            RegionName = regionName,
                            IsMuted = regionName == "@ToBeRemoved",
                        });
                }

                //check for region end
                if (RegexRegionEnd.IsMatch(templateLines[index]))
                {
                    IsRegionEnd = true;
                    index++;
                    return;
                }

                //normal template
                // IsTemplatedLine = true;
                Templates.Add(new TemplateEngine(templateLines[index]));
                index++;
            }
        }


        /// <summary>
        ///     Initializes a new instance of the <see cref="TemplateEngine" /> class.
        /// </summary>
        /// <param name="templatedLine">The line.</param>
        private TemplateEngine(string templatedLine)
        {
            _templatedLine = templatedLine;
            IsTemplatedLine = true;
        }

        /// <summary>
        ///     Gets or sets a value indicating whether this instance is muted.
        /// </summary>
        /// <value>
        ///     <c>true</c> if this instance is muted; otherwise, <c>false</c>.
        /// </value>
        public bool IsMuted
        {
            get { return _isMuted; }
            set
            {
                _isMuted = value;
                if (_isMuted)
                {
                    Templates.ForEach(t => t.IsMuted = _isMuted);
                }
            }
        }

        public bool IsRegionStart { get; private set; }
        public bool IsRegionEnd { get; private set; }
        public bool IsTemplatedLine { get; private set; }

        /// <summary>
        ///     Gets or sets the region name.
        /// </summary>
        /// <value>
        ///     The name.
        /// </value>
        public string RegionName { get; set; }

        /// <summary>
        ///     Gets the templates.
        /// </summary>
        /// <value>
        ///     The templates.
        /// </value>
        public List<TemplateEngine> Templates
        {
            get { return _templates; }
        }

        public string TemplatedLine
        {
            get { return _templatedLine; }
        }

        public int CollectionIndex { get; set; }

        /// <summary>
        ///     Creates the template line.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="property">The property.</param>
        /// <param name="prefix">The prefix.</param>
        /// <param name="postfix">The postfix.</param>
        /// <param name="renderPrefix">
        ///     if set to <c>true</c> [render prefix].
        /// </param>
        /// <param name="renderPostfix">
        ///     if set to <c>true</c> [render postfix].
        /// </param>
        /// <returns></returns>
        public static string CreateTemplateLine<T>(Expression<Func<T, object>> property, string prefix = null,
                                                   string postfix = null, bool renderPrefix = false,
                                                   bool renderPostfix = false)
        {
            string membername = ExpressionHelper.GetFullPropertyName(property, "_");
            var sb = new StringBuilder("@");
            //check if prefix is supplied
            if (prefix != null)
            {
                if (renderPrefix) //if rendering prefix append it to the start
                    sb.Append(TemplatePrefixCharacter);
                sb.Append(prefix);

                sb.Append(TemplatePrefixCharacter);
            }
            sb.Append(membername);

            if (postfix != null)
            {
                sb.Append(TemplatePrefixCharacter);
                sb.Append(prefix);
                if (renderPostfix)
                    sb.Append(TemplatePrefixCharacter);
            }


            return sb.ToString();
        }

        /// <summary>
        ///     Renders the specified data context.
        /// </summary>
        /// <param name="dataContext">The data context.</param>
        /// <param name="streamWriter">The stream writer.</param>
        public void Render(object dataContext, StreamWriter streamWriter)
        {
            if (IsMuted)
                return;

            //resolve the datacontext
            object resolvedDataContext = ResolveDataContext(dataContext);

            if (TemplatedLine != null)
            {
                streamWriter.WriteLine(ResolveLine(resolvedDataContext, TemplatedLine));
            }
            else //TemplatedLine is null because it is RegionStart or Region end... Have RegionName here...
            {
                if (resolvedDataContext is ICollection)
                {
                    int index = 1;
                    foreach (object dataContextElement in resolvedDataContext as ICollection)
                    {
                        foreach (TemplateEngine template in Templates)
                        {
                            template.CollectionIndex = index;
                            template.Render(dataContextElement, streamWriter);
                        }

                        index++;
                    }
                }
                else
                {
                    foreach (TemplateEngine template in Templates)
                    {
                        template.Render(resolvedDataContext, streamWriter);
                    }
                }
            }
        }

        private object ResolveObject(object dataContext, string path)
        {
            if (path == null)
                return dataContext;

            object data = dataContext;

            if (path.StartsWith("ßEngineß"))
            {
                path = path.Substring(8);
                data = this;
            }

            if (path.StartsWith("@"))
            {
                path = path.Substring(1);
                //data = this;
            }

            string[] segments = path.Split(new[] {'.', '_'});

            foreach (string segment in segments)
            {
                PropertyInfo pi = data.GetType().GetProperty(segment);
                if (pi == null)
                    throw new ArgumentException(
                        string.Format("Cannot find segment {0} on data: {1}", segment, data),
                        "path");

                data = pi.GetValue(data, null);
            }
            return data;
        }

        /// <summary>
        ///     Resolves the data context.
        /// </summary>
        /// <param name="dataContext">The data context.</param>
        /// <returns></returns>
        private object ResolveDataContext(object dataContext)
        {
            if (RegionName == null || RegionName == "@ToBeRemoved")
                return dataContext;


            string propertyName = GetPropertyName();

            return ResolveObject(dataContext, propertyName);
        }

        /// <summary>
        ///     Gets the name of the property.
        /// </summary>
        /// <returns></returns>
        /// <exception cref="System.InvalidOperationException">propertyName is not resolved!</exception>
        private string GetPropertyName()
        {
            //get property name
            string propertyName = null;

            //check if line is region start or end
            if (TemplatedLine == null)
            {
                propertyName = RegionName; //.Replace("@", "");
            }
            else
            {
                propertyName = RegexProperty.Match(TemplatedLine).Groups[2].Value;
            }

            if (propertyName == null)
                throw new InvalidOperationException("propertyName is not resolved!");
            return propertyName;
        }

        /// <summary>
        ///     Resolves the line.
        /// </summary>
        /// <param name="datacontext">The datacontext.</param>
        /// <param name="rawLine">The raw line.</param>
        /// <returns></returns>
        public string ResolveLine(object datacontext, string rawLine)
        {
            string line = string.Empty;

            line = RegexEngineProperty.Replace(rawLine, RegexEnginePropertyMatchEvaluator(datacontext, rawLine));
            line = RegexProperty.Replace(line, RegexPropertyMatchEvaluator(datacontext, rawLine));


            return line;
        }

        private MatchEvaluator RegexPropertyMatchEvaluator(object datacontext, string rawLine)
        {
            return match =>
                {
                    string prefix = match.Groups[1].Value;
                    string propertyName = match.Groups[2].Value;
                    string postfix = match.Groups[3].Value;

                    try
                    {
                        object value = ResolveObject(datacontext,
                                                     propertyName);

                        prefix = prefix.StartsWith(TemplatePrefixCharacter)
                                     ? prefix.Replace(
                                         TemplatePrefixCharacter,
                                         string.Empty)
                                     : string.Empty;

                        postfix = postfix.EndsWith(TemplatePostfixCharacter)
                                      ? postfix.Replace(
                                          TemplatePostfixCharacter,
                                          string.Empty)
                                      : string.Empty;

                        return new StringBuilder(prefix)
                            .Append(value.AsCSharpSourceCode())
                            .Append(postfix).ToString();
                    }
                    catch (Exception ex)
                    {
                        throw new InvalidOperationException(
                            string.Format(
                                "property name {0} error at rawLine: {1}", propertyName, rawLine),
                            ex);
                    }
                };
        }

        private MatchEvaluator RegexEnginePropertyMatchEvaluator(object datacontext, string rawLine)
        {
            return match =>
                {
                    string prefix = match.Groups[1].Value;
                    string propertyName = match.Groups[2].Value;
                    string postfix = match.Groups[3].Value;
                    try
                    {
                        object value = ResolveObject(datacontext,
                                                     propertyName);

                        prefix = prefix.StartsWith(TemplatePrefixCharacter)
                                     ? prefix.Replace(
                                         TemplatePrefixCharacter,
                                         string.Empty)
                                     : string.Empty;

                        postfix = postfix.StartsWith(TemplatePostfixCharacter)
                                      ? postfix.Replace(
                                          TemplatePostfixCharacter,
                                          string.Empty)
                                      : string.Empty;

                        return new StringBuilder(prefix)
                            .Append(value.AsCSharpSourceCode())
                            .Append(postfix).ToString();
                    }
                    catch (Exception ex)
                    {
                        throw new InvalidOperationException(
                            string.Format(
                                "property name {0} error at line: {1}", propertyName, rawLine),
                            ex);
                    }
                };
        }

        /// <summary>
        ///     Renders the specified lines.
        /// </summary>
        /// <param name="lines">The lines.</param>
        /// <param name="dataContext">The data context.</param>
        /// <returns></returns>
        public static string Render(string[] lines, object dataContext)
        {
            MemoryStream ms = null;
            try
            {
                //grab a memory stream
                ms = new MemoryStream();

                //grab a stream writer
                using (var sw = new StreamWriter(ms, Encoding.UTF8))
                {
                    var template = new TemplateParser(lines);
                    template.RootTemplateEngine.Render(dataContext, sw);

                    sw.Flush();
                    ms.Seek(0, SeekOrigin.Begin);

                    byte[] bytes = ms.ToArray();
                    string code = Encoding.UTF8.GetString(bytes);

                    ms = null;
                    return code;
                }
            }
            finally
            {
                if (ms != null)
                    ms.Dispose();
            }
        }
    }
}